<?php

namespace Gnway\Ticket\Rest\Container;

use Closure;
use Gnway\Ticket\Rest\Contracts\ContainerInterface;
use Gnway\Ticket\Rest\Exceptions\ClassNoDefException;
use ReflectionException, RuntimeException, InvalidArgumentException, Exception;
use ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionNamedType;

/**
 * PHPIoc library
 *
 * Simple example usage:
 * RestContainer::make(your class name);
 *
 * @author Shaolong Zu
 * @mail zushaolong@gmail.com
 */
class RestContainer implements ContainerInterface
{

  /**
   * Resolved container.
   *
   * @var array
   */
  protected static array $container = [];

  /**
   * Class alias name.
   *
   * @var array
   */
  protected static array $alias = [];

  /**
   * Reflection classes
   *
   * @var ReflectionClass[]
   */
  protected static array $reflectionClasses = [];

  /**
   * Reflection methods
   *
   * @var ReflectionMethod[]
   */
  protected static array $reflectionMethods = [];

  /**
   * Classes scope
   *
   * @var array
   */
  protected static array $scopeMap = [];

  /**
   * Don't let anyone instantiate this class
   */
  private function __construct() { }

  /**
   * Make Instance.
   *
   * @param string $class
   * @param array  $args
   * @return mixed
   * @throws ReflectionException
   * @throws RuntimeException
   */
  public static function make(string $class, array $args = [])
  {
    if (!class_exists($class) && !interface_exists($class)) {
      throw new ClassNoDefException(sprintf('Class %s was not found.', $class));
    }

    //Resolve instance from container.
    $instance = self::resolve($class);

    if ($instance === NULL && ($aliasClass = self::getAliasClass($class)) !== NULL) {
      $class = $aliasClass;
    }

    //Make instance.
    if ( ! ($instance instanceof $class)) {
      $rf = self::getClassReflector($class);
      if ( ! $rf->isInstantiable()) {
        throw new RuntimeException(sprintf('Class %s cannot be instantiated.', $class));
      }
      elseif ($rf->hasMethod('__construct')) {
        $params = self::getMethodParameters($class, '__construct', $args);
        $instance = $rf->newInstanceArgs($params);
      }
      else {
        $instance = new $class;
      }

      //Bind object to container
      self::bind($class, $instance);
    }

    return $instance;
  }

  /**
   * Resolve instance from container.
   *
   * @param string $class
   * @return mixed
   */
  public static function resolve(string $class)
  {
    $instance = NULL;

    $aliasClass = self::getAliasClass($class);
    if ($aliasClass !== NULL) {
      $class = $aliasClass;
    }

    if (isset(self::$container[$class])) {
      $instance = self::$container[$class];

      if (isset(self::$scopeMap[$class]) && self::$scopeMap[$class] !== ClassScope::SINGLETON) {
        self::unbind($class);
      }
    }

    return $instance;
  }

  /**
   * Bind instance to container.
   *
   * @param string $class
   * @param object $instance
   * @return object
   * @throws RuntimeException
   */
  public static function bind(string $class, object $instance): object
  {
    if ( ! class_exists($class)) {
      throw new ClassNoDefException(sprintf('Class %s was not found.', $class));
    }
    elseif ( ! ($instance instanceof $class)) {
      throw new RuntimeException(sprintf('Given instance must instanceof %s, %s give.',
        $class, $instance ? get_class($instance) : 'null'));
    }

    if (isset(self::$scopeMap[$class]) && self::$scopeMap[$class] !== ClassScope::SINGLETON) {
      return $instance;
    }

    self::$container[$class] = $instance;

    return $instance;
  }

  /**
   * Unbind instance from container.
   *
   * @param string $class
   * @return void
   * @throws ClassNoDefException
   */
  public static function unbind(string $class): void
  {
    if ( ! class_exists($class)) {
      throw new ClassNoDefException(sprintf('Unbind Exception: class %s was not found.', $class));
    }

    self::removeClassAlias($class);

    unset(self::$container[$class]);
  }

  /**
   * Get reflection class
   *
   * @param string|object $class
   * @return null|ReflectionClass
   * @throws ReflectionException
   */
  public static function getClassReflector($class): ?ReflectionClass
  {
    is_string($class) || $class = get_class($class);
    if ( ! isset(self::$reflectionClasses[$class])) {
      self::$reflectionClasses[$class] = new ReflectionClass($class);
    }

    return self::$reflectionClasses[$class];
  }

  /**
   * Get reflection method
   *
   * @param string|object $class
   * @param string        $method
   * @return null|ReflectionMethod
   * @throws ReflectionException
   */
  public static function getMethodReflector($class, string $method): ?ReflectionMethod
  {
    $signature = (is_object($class) ? get_class($class) : $class) . '::' . $method;
    if ( ! isset(self::$reflectionMethods[$signature])) {
      self::$reflectionMethods[$signature] = new ReflectionMethod($signature);
    }

    return self::$reflectionMethods[$signature];
  }

  /**
   * Resolve the parameters of a specified method.
   *
   * @param string|object $class
   * @param string $method
   * @param array $default
   * @return array
   * @throws ReflectionException
   */
  public static function getMethodParameters($class, string $method, array $default = []): array
  {
    return self::_resolveParameters(self::getMethodReflector($class, $method), $default);
  }

  /**
   * Resolve the parameters of a specified function.
   *
   * @param string|Closure $function
   * @param array $default
   * @return array
   * @throws ReflectionException
   */
  public static function getFunctionParameters($function, array $default = []): array
  {
    return self::_resolveParameters(new ReflectionFunction($function), $default);
  }

  /**
   * Resolve the parameters of a specified function or method.
   *
   * @param ReflectionFunctionAbstract $parameters
   * @param array                      $default
   * @return array
   * @throws ReflectionException
   * @throws Exception
   */
  protected static function _resolveParameters(ReflectionFunctionAbstract $parameters, array $default = []): array
  {
    $args = [];

    foreach ($parameters->getParameters() as $key => $parameter) {
      if (is_int($key) && array_key_exists($key, $default)) {
        $args[$key] = $default[$key];

        continue;
      }
      elseif (isset($default[($name = $parameter->getName())])) {
        $args[] = $default[$name];

        continue;
      }
      if ( ! $parameterClass = $parameter->getClass()) {
        if ($parameter->isDefaultValueAvailable()) {
          $args[] = $parameter->getDefaultValue();
        }
        elseif ($parameter->allowsNull()) {
          $args[] = NULL;
        }
        else {
          $args[] = ($type = $parameter->getType()) instanceof ReflectionNamedType
            ? self::_getTypeDefaultValue($type->getName())
            : NULL;
        }
      }
      else {
        $args[] = self::make($parameterClass->getName());
      }
    }

    return $args;
  }

  /**
   * Gets its default value based on the data type.
   *
   * @param string $type
   * @return array|bool|float|int|null|string
   * @throws Exception
   */
  protected static function _getTypeDefaultValue(string $type)
  {
    switch (strtolower($type))
    {
      case 'int':
      case 'integer':
        return 0;
      case 'float':
      case 'double':
        return 0.00;
      case 'string':
        return '';
      case 'bool':
      case 'boolean':
        return FALSE;
      case 'array':
        return [];
      case 'callable':
      case 'null':
      case 'object':
      case 'resource':
      case 'resource (closed)':
      case 'unknown type':
        return NULL;
      default:
        throw new Exception(sprintf('TypeError: No such built-in type %s.', $type));
    }
  }

  /**
   * Get class alias
   *
   * @param string $class
   * @return string|null
   */
  protected static function getClassAlias(string $class): ?string
  {
    if (empty(self::$alias)) {
      return NULL;
    }

    $alias = array_search($class, self::$alias, TRUE);

    return $alias !== FALSE ? $alias : NULL;
  }

  /**
   * Get alias class
   *
   * @param string $alias
   * @return string|null
   */
  protected static function getAliasClass(string $alias): ?string
  {
    if (isset(self::$alias[$alias]) && is_subclass_of(self::$alias[$alias], $alias)) {
      return self::$alias[$alias];
    }

    return NULL;
  }

  /**
   * Remove class alias
   *
   * @param string $class
   * @return void
   */
  protected static function removeClassAlias(string $class)
  {
    $alias = self::getClassAlias($class);
    if ($alias === NULL) {
      return;
    }

    unset(self::$alias[$alias]);

    self::removeClassAlias($class);
  }

  /**
   * Set classes scope
   *
   * @param string $class
   * @param string $scope
   * @return void
   */
  public static function setScope(string $class, string $scope): void
  {
    if (!$class) {
      throw new InvalidArgumentException('Class name cannot be empty');
    }
    if (!ClassScope::contains($scope)) {
      throw new InvalidArgumentException('Class scope value invalid');
    }

    self::$scopeMap[$class] = $scope;
  }

  /**
   * Set class alias
   *
   * @param string $class
   * @param string $alias
   * @return void
   */
  public static function setClassAlias(string $class, string $alias): void
  {
    if (empty($class)) {
      throw new InvalidArgumentException('Class name cannot be empty');
    }
    elseif (empty($alias)) {
      throw new InvalidArgumentException('Alias name cannot be empty');
    }
    elseif (isset(self::$alias[$alias]) && self::$alias[$alias] !== $class) {
      throw new RuntimeException('The alias ' . $alias . ' has been bound by class ' . self::$alias[$alias]);
    }

    self::$alias[$alias] = $class;
  }
}